﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ProLinq.Projection.Configuration
{
	/// <summary>
	///     Contains methods to create implicit configurations
	/// </summary>
	public class ProjectionConfigurationFactory : IProjectionConfigurationFactory
	{
		#region Static

		private static IProjectionConfigurationFactory _instance;

		/// <summary>
		///     Instance of the configuration factory
		/// </summary>
		public static IProjectionConfigurationFactory Instance
		{
			get
			{
				if (_instance == null)
				{
					_instance = new ProjectionConfigurationFactory();
				}
				return _instance;
			}
			set { _instance = value; }
		}

		#endregion

		#region Helpers

		// TODO: make GetMemberType an extension method or something..
		private Type GetMemberType(MemberInfo member)
		{
			if (member is FieldInfo)
				return ((FieldInfo) member).FieldType;
			if (member is PropertyInfo)
				return ((PropertyInfo) member).PropertyType;
			return null;
		}

		private IEnumerable<MemberInfo> GetPropertiesAndFields(Type type)
		{
			BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy;

			return from m in type.GetMembers(bindingFlags)
			       where m is PropertyInfo || m is FieldInfo
			       select m;
		}

		private IEnumerable<PropertyMap> CreateMap(Type sourceType, Type resultType)
		{
			IEnumerable<MemberInfo> sourceMembers = this.GetPropertiesAndFields(sourceType);
			IEnumerable<MemberInfo> resultMembers = this.GetPropertiesAndFields(resultType);

			IEnumerable<PropertyMap> maps = from s in sourceMembers
			                                join r in resultMembers on new {s.Name, TYpe = this.GetMemberType(s)} equals
				                                new {r.Name, TYpe = this.GetMemberType(r)}
			                                select new PropertyMap(s, r);

			return maps;
		}

		#endregion

		#region Fields

		private readonly ProjectionConfigurationCache _cache = new ProjectionConfigurationCache();

		#endregion

		#region Methods

		private ProjectionConfiguration<TSource, TResult> CreateNewConfiguration<TSource, TResult>(
			Expression<Func<TSource, TResult>> factory)
		{
			var configuration = new ProjectionConfiguration<TSource, TResult>();
			configuration.MainProjection.Factory = factory.Compile();

			if (factory.Body is MemberInitExpression)
			{
				var memberInit = factory.Body as MemberInitExpression;
				
				var maps = memberInit.Bindings.OfType<MemberAssignment>()
					          .Where(b => b.Expression.NodeType == ExpressionType.MemberAccess)
					          .Select(b =>
						          {
							          var srcExpr = (MemberExpression) b.Expression;
							          return new PropertyMap(srcExpr.Member, b.Member);
						          });

				configuration.MainProjection.Map = new PropertyMapCollection(typeof(TSource), typeof(TResult), maps.ToList());
			}
			else
			{
				var maps = this.CreateMap(typeof (TSource), typeof (TResult)).ToArray();
				configuration.MainProjection.Map = new PropertyMapCollection(typeof(TSource), typeof(TResult), maps.ToList());
			}

			return configuration;
		}

		private ProjectionConfiguration<TSource, TResult> CreateNewConfiguration<TSource, TResult>()
		{
			var config = new ProjectionConfiguration<TSource, TResult>();
			IEnumerable<PropertyMap> maps = this.CreateMap(typeof (TSource), typeof (TResult));

			IEnumerable<PropertyMap> witeableProps = from map in maps
			                                         where
				                                         map.DestinationProperty is FieldInfo ||
				                                         ((PropertyInfo) map.DestinationProperty).CanWrite
			                                         select map;

			ParameterExpression param = Expression.Parameter(typeof (TSource), "s");

			MemberInitExpression initExpr = Expression.MemberInit(Expression.New(typeof (TResult)), from map in witeableProps
			                                                                                        select
				                                                                                        (MemberBinding)
				                                                                                        Expression.Bind(
					                                                                                        map.DestinationProperty,
					                                                                                        Expression.MakeMemberAccess
						                                                                                        (param, map.SourceProperty)));

			Expression<Func<TSource, TResult>> ctrExpr = Expression.Lambda<Func<TSource, TResult>>(initExpr, param);
			config.MainProjection.Factory = ctrExpr.Compile();
			config.MainProjection.Map = new PropertyMapCollection(typeof(TSource), typeof(TResult), maps.ToList());
			return config;
		}

		#endregion

		#region ProjectionConfigurationFactory Members

		public ProjectionConfiguration<TSource, TResult> CreateConfiguration<TSource, TResult>(
			Expression<Func<TSource, TResult>> factory)
		{
			return this.CreateNewConfiguration(factory);
		}

		public ProjectionConfiguration<TSource, TResult> CreateConfiguration<TSource, TResult>()
		{
			// TODO: do something with configuration caching
			ProjectionConfiguration<TSource, TResult> cfg = this._cache.GetConfiguration<TSource, TResult>();
			if (cfg == null)
			{
				cfg = this.CreateNewConfiguration<TSource, TResult>();
				_cache.SetConfiguration(cfg);
				return cfg;
			}
			else
			{
				// return copy of cached configuration, so that caller cannot change it
				return (ProjectionConfiguration<TSource, TResult>)cfg.Clone();
			}
		}

		public ProjectionConfiguration CreateConfiguration()
		{
			return new ProjectionConfiguration();
		}

		#endregion
	}
}